import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
 * Simple Multi-Cycle Processer Assembler
 * Project 2, CS 3220
 * 
 * This code converts assembly code (.asm) to a MIF "binary" file that can be used by quartus II
 * @author Dan Moore
 *
 */
public class Assembler {

	public BufferedReader in,tmpIn;
	public BufferedWriter out,tmpOut;
	
	/**
	 * Globals. Used for recurring labels (immediates) and the current memory line)
	 * Each memory increase stands for 
	 */
	public static String currentAddr, startAddr;
	public static Map<String,String> labels;
	
	public Assembler(String[] args) throws IOException {
		
		
		in = new BufferedReader(new FileReader(args[0]+".asm"));
		System.out.println("Opened assembly file: "+args[0]+".asm.");
		
		tmpOut = new BufferedWriter(new FileWriter(args[0]+".tmp"));
		System.out.println("Created temporary file: "+args[0]+".tmp.");
				
		labels = new HashMap<String,String>();
		
		processMeta();
		System.out.println("Pre-Processed assembly file: "+args[0]+".asm.");
		
		out = new BufferedWriter(new FileWriter(args[0]+".mif"));
		System.out.println("Created memory file: "+args[0]+".mif.");
		
		tmpIn = new BufferedReader(new FileReader(args[0]+".tmp"));
		System.out.println("Opened temporary file: "+args[0]+".tmp.");
		
		processTmp(args);
		System.out.println("Processed memory file: "+args[0]+".mif.");
		
		cleanUp();
		System.out.println("Cleaned up.");
	}
	
	public void processMeta() throws IOException {
		String toWrite = null;
		while(in.ready()) {
			if((toWrite = firstRound(in.readLine())) != null) {
				tmpOut.write(toWrite+"\n");
			}
		}
		tmpOut.flush();
	}
	
	public void processTmp(String[] args) throws IOException {
		String toWrite = null;
		int tmp;
		
		//Headers
		out.write("WIDTH="+args[1]+";\n");
		out.write("DEPTH="+args[2]+";\n");
		out.write("ADDRESS_RADIX=HEX;\n");
		out.write("DATA_RADIX=HEX;\n");
		out.write("CONTENT BEGIN\n");
		
		tmp = Integer.parseInt(startAddr,16)-1;
		toWrite = hexStrSizer(8,Integer.toHexString(tmp));
		
		out.write("[00000000.."+toWrite+"] : DEADBEEF;\n");
		
		while(tmpIn.ready()) {
			if((toWrite = secondRound(tmpIn.readLine())) != null) {
				out.write(toWrite+";\n");
			}
		}
		
		//Footers
		tmp = Integer.parseInt(currentAddr,16) + 1;
		toWrite = hexStrSizer(8,Integer.toHexString(tmp));
		out.write("["+toWrite+"..");
		
		tmp = Integer.parseInt(args[2]) - 1;
		toWrite = hexStrSizer(8,Integer.toHexString(tmp));
		out.write(toWrite+"] : DEADBEEF;\n");
		out.write("END;\n");
		
		out.flush();
	}
	
	public void cleanUp() throws IOException {
		tmpOut.close();
		tmpIn.close();
		out.close();
		in.close();
	}
	
	public String incrementAddr(String curr) {
		int val = Integer.parseInt(curr, 16);
		val++;
		return Integer.toHexString(val);
	}
	
	public String translateReg(String reg) {
		reg = reg.toUpperCase();
		if(reg.equals("T0") || reg.equals("R0"))
			return "0";
		else if(reg.equals("T1") || reg.equals("R1"))
			return "1";
		else if(reg.equals("T2") || reg.equals("R2"))
			return "2";
		else if(reg.equals("T3") || reg.equals("R3"))
			return "3";
		else if(reg.equals("A0") || reg.equals("R4"))
			return "4";
		else if(reg.equals("A1") || reg.equals("R5"))
			return "5";
		else if(reg.equals("A2") || reg.equals("R6"))
			return "6";
		else if(reg.equals("A3") || reg.equals("R7"))
			return "7";
		else if(reg.equals("RV") || reg.equals("R8"))
			return "8";
		else if(reg.equals("GP") || reg.equals("S0") || reg.equals("R9"))
			return "9";
		else if(reg.equals("RA") || reg.equals("S1") || reg.equals("RA"))
			return "A";
		else if(reg.equals("FP") || reg.equals("S2") || reg.equals("RB"))
			return "B";
		else if(reg.equals("SP") || reg.equals("RF"))
			return "F";
		
		return "X";
	}
	
	public String handleImm(String imm,boolean jrl_op,boolean f2) {
		String reg = null, numlab;
		int val,currVal = 0;
		if(imm.contains("(") && imm.contains(")")) {
			String[] tokens = imm.split("\\(");
			numlab = tokens[0];
			reg = tokens[1].substring(0, 2);
			//System.out.println("Reg: "+reg);
			reg = translateReg(reg);
			//System.out.println("NUM/LAB: "+numlab);
			//System.out.println("Reg: "+reg);
			//for(int i = 0; i < tokens.length; i++) {
			//	System.out.println(i+": "+tokens[i]);
			//}
		} else {
			numlab = imm;
		}
		
		if(!numlab.contains("0x") && numlab.matches("[A-Za-z]+([0-9]*[A-Za-z]*)*")) {
			//System.out.println("Got a label!");
			val = Integer.parseInt(labels.get(numlab),16);
			//System.out.println("-Parsed: "+val);
			if(!f2 && reg == null) { //PC relative 
				currVal = Integer.parseInt(currentAddr,16);
				//System.out.println("-Curr Addr: "+currVal);
				val = val - (currVal+1);
				//System.out.println("-Diff: "+val);
			} else if (!jrl_op){
				val *= 4;
			}
		} else {
			if(numlab.contains("0x")) {
				numlab = numlab.substring(2);
				val = Integer.parseInt(numlab,16);
			} else {
				val = Integer.parseInt(numlab);
			}
		}
		
		//val /= 4; //TODO: Divide by 4?		
		numlab = hexStrSizer(4,Integer.toHexString(val));
		//System.out.println("-Printing: "+numlab);
		//System.out.println("Reg: "+reg+". Val: "+val+". Hex of Val: "+Integer.toString(val, 16)+". sxt hex val: "+numlab+".");
		if(reg == null)
			return numlab;
		return reg+numlab;		
	}
	
	public String parseF1(String regList) {
		String[] tokens = regList.split(",");
		String toRet = "";
		for(int i = 0; i < 3; i++)
			toRet +=  translateReg(tokens[i]);
		return toRet;
	}
	
	public String parseF2(String regList,boolean f2) {
		String[] tokens = regList.split(",");
		String toRet = translateReg(tokens[0]);
		toRet += translateReg(tokens[1]);
		toRet += handleImm(tokens[2],false,f2);
		return toRet;
	}
	
	//If the register list is of the form Rh,#/Label(Rh), translate it to 
	public String parseF3(String regList, boolean jrl_op) {
		String[] tokens = regList.split(",");
		String toRet = translateReg(tokens[0]);
		//toRet += translateReg(tokens[1]); //the reg is part of the immediate?
		toRet += handleImm(tokens[1],jrl_op,false);
		return toRet;
	}
	
	public String parseF4(String regList) {
		String[] tokens = regList.split(",");
		String toRet = translateReg(tokens[0]);
		toRet += "0";
		toRet += handleImm(tokens[1],false,false);
		return toRet;
	}
	
	public String hexStrSizer(int size, String curr) {
		int len = curr.length();
		
		if(len > size) { //concat LSB
			return curr.substring(len-size);
		}
		
		char toPadd = '0';
		//String first = ""+curr.charAt(0); //allows for signed padding
		//if(first.matches("[89A-Fa-f]"))
		//	toPadd = 'F';
		
		
		String toRet = "";
		for(int i = 0; i < size-len; i++ ) {
			toRet+=toPadd;
		}
		toRet += curr;
		return toRet;
	}
	
	/**
	 * Removes all comments.
	 * Processes all .ORIG / hacks
	 * Processes address list to "half processed" lines of correct addresses and unprocessed commands
	 * @param toProcess
	 * @return the half processed line
	 */
	public String firstRound(String toProcess) {
		String[] tokens;
		String toRet = null; 
		toProcess = toProcess.replaceAll("(\\s)+", "\t"); //replaces spaces with a single tab char
		if(toProcess.contains(";")) { //chop off comment
			tokens = toProcess.split(";");
			toProcess = tokens[0];
		}
		//System.out.println("Processing....:"+toProcess);
		
		if(toProcess.length() > 1) {
			if(toProcess.toUpperCase().contains(".ORIG")) {
				tokens = toProcess.split("\t");
				int tmp;
				if(tokens[2].contains("0x")) {
					tokens[2] = tokens[2].substring(2);
					tmp = Integer.parseInt(tokens[2],16);
				} else {
					tmp = Integer.parseInt(tokens[2]);
				}
				
				tmp /= 4;
				tokens[2] = Integer.toHexString(tmp);
				currentAddr = startAddr = tokens[2];
			} else if(toProcess.contains(":")) { //it's a label
				tokens = toProcess.split(":");
				labels.put(tokens[0], currentAddr);
			} else {
				if (!toProcess.matches("((\\s)*[^A-Za-z0-9]*)*")) { //only space characters
					toRet = hexStrSizer(8,currentAddr)+":"+toProcess.substring(1);
					currentAddr = incrementAddr(currentAddr);
				}
			}
		}
		return toRet;
	}
	
	/**
	 * Takes a half processed line and processes the op/reglist half
	 * 
	 * @param toProcess
	 * @return the fully processed line
	 */
	public String secondRound(String toProcess) {
		String[] tokens;
		String addr;
		
		tokens = toProcess.split(":");
		
		addr = currentAddr = tokens[0];
		toProcess = toProcess.replaceAll("(\\s)+", "\t"); //replaces spaces with a single tab char
		tokens = tokens[1].split("\t");
		
		for(int i = 0; i < tokens.length; i++) {
			//System.out.println(i+": "+tokens[i]);
		}
		
		if(tokens[0].toUpperCase().contains(".WORD")) {
			int num;
			if(tokens[1].startsWith("0x")) {
				tokens[1] = tokens[1].substring(2);
				tokens[1] = hexStrSizer(8,tokens[1]);
			} else if (tokens[1].matches("[A-Za-z]+")) {
				tokens[1] = labels.get(tokens[1]);
				num = Integer.parseInt(tokens[1],16);
				num *= 4;
				tokens[1] = Integer.toHexString(num);
				tokens[1] = hexStrSizer(8,tokens[1]);
			} else {
				num = Integer.parseInt(tokens[1]);
				tokens[1] = Integer.toHexString(num);
				tokens[1] = hexStrSizer(8,tokens[1]);
			}
			return addr + " : " + tokens[1];
		}
	
		OpCode op = OpCode.getOp(tokens[0].toUpperCase());
		String toPrint = "";
		switch(op) {
			case ADD:
			case SUB:
			case AND:
			case NAND:
			case OR:
			case NOR:
			case XOR:
			case EQ:
			case NE:
			case LT:
			case LE:
				toPrint = op.getHex()+parseF1(tokens[1])+"000";
				break;
			case ADDI:
			case SUBI:
			case ANDI:
			case NANDI:
			case ORI:
			case NORI:
			case XORI:
			case EQI:
			case NEI:
			case LTI:
			case LEI:
				toPrint = op.getHex()+parseF2(tokens[1],true);
				break;
			case BEQ:
			case BNE:
				toPrint = op.getHex()+parseF2(tokens[1],false);
				break;
			case JRL:
				toPrint = op.getHex()+parseF3(tokens[1],true);
				break;
			case LW:
			case SW:
				toPrint = op.getHex()+parseF3(tokens[1],false);
				break;
			case BEQZ:
			case BNEZ:
				toPrint = op.getHex()+parseF4(tokens[1]);
				break;
			case NOP:
				toPrint = "00000000";
				break;
		}
		return addr.toLowerCase()+" : "+toPrint.toLowerCase();
	}

	public Assembler() {
		//Some Test Cases
		//labels = new HashMap<String,String>();
		//labels.put("Label1", "00000123");
		//labels.put("Labels", "00000500");
		//labels.put("Error", "00000777");
		//System.out.println(handleImm("0(T3)"));
		//System.out.println(handleImm("Labels(T3)"));
		//System.out.println(handleImm("Error"));
		//System.out.println(handleImm("15"));
		//System.out.println(handleImm("0xF"));
		//orig("	.ORIG 2048");
		//label("ASize:	; Size of the array is 512 elements (2048 bytes) ");
		//out.write("	BEQZ	RV,Error");
	}

	public static void main(String[] args) throws IOException {
		if(args.length != 3) {
			//new Assembler();
			System.err.println("Operating Arguments Required: ");
			System.err.println("\tjava Assembler FILE_NAME WIDTH DEPTH");
			System.err.println("Example: ");
			System.err.println("\tjava Assembler Assign2 32 2048");
			System.exit(1);
		} else {
			new Assembler(args);
			System.out.println("Done.");
		}
	}
}
